﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Windows.Threading;
using System.Dynamic;

namespace MVVMFramework.ViewModel
{
    /// <summary>
    /// Base implementation of INotifyPropertyChanged interface with ability to mark dependent properties.
    /// </summary>
    public abstract class NotifyPropertyChangedBase : DynamicObject, INotifyPropertyChanged, INotifyDataErrorInfo, IValidationInfo
    {
        /// <summary>
        /// Holds dictionary of direct dependencies (whos master object is this and there is no property path).
        /// Direct dependencies are handled by OnPropertyChanged routine.
        /// </summary>
        private Dictionary<string, List<string>> dependencies;

        private HashSet<IValidator> validators;

        /// <summary>
        /// Gets the dispatcher. 
        /// Dispatcher is used to determine whether the calling thread is the thread associated with this <see cref="T:System.Windows.Threading.Dispatcher"/> 
        /// and execute methods on the thread the <see cref="T:System.Windows.Threading.Dispatcher"/> is associated with.
        /// </summary>
        protected Dispatcher Dispatcher;

        protected NotifyPropertyChangedBase()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="NotifyPropertyChangedBase"/> class.
        /// </summary>
        /// <param name="dispatcher">The background threads must delegate the work to the dispatcher associated with the current instance </param>
        protected NotifyPropertyChangedBase(Dispatcher dispatcher)
        {
            Dispatcher = dispatcher;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>		
        /// Call OnPropertyChanged after property change.
        /// </summary>									
        protected virtual void OnPropertyChanged(string property)
        {
            if (property == null)
                throw new ArgumentNullException("property");

            // Notify all subscribers
            PropertyChangedEventHandler propertyChanged = PropertyChanged;
            if (propertyChanged != null)
            {
                if (Dispatcher != null && Dispatcher.Thread != Thread.CurrentThread)
                    Dispatcher.Invoke(new Action<string>(OnPropertyChanged), property);
                else
                    propertyChanged(this, new PropertyChangedEventArgs(property));
            }

            // Recursively traverse all direct dependencies
            if (dependencies != null)
            {
                List<string> dependentProperties;
                if (dependencies.TryGetValue(property, out dependentProperties))
                {
                    foreach (string dependentProperty in dependentProperties)
                    {
                        OnPropertyChanged(dependentProperty);
                    }
                }
            }
        }

        /// <summary>		
        /// Call OnPropertyChanged after property change.
        /// </summary>									
        /// <param name="property">
        /// Lambda expression to get property. <example>() => Property</example>
        /// </param>
        protected void OnPropertyChanged(Expression<Func<object>> property)
        {
            if (property == null)
                throw new ArgumentNullException("property");

            string propertyName = GetPropertyName(property);

            OnPropertyChanged(propertyName);
        }

        /// <summary>
        /// Mark dependent property to be notified after master property change.
        /// Warning: Not thead safe. You should register all dependencies in the constructor 
        /// before any property change can occur in other thread.
        /// Circular dependencies are not checked.
        /// </summary>
        /// <param name="masterProperty">Master Property or property path (like Parent.Child.Value)</param>
        /// <param name="dependentProperty">This property will be notified after Master Property change</param>
        protected void RegisterDependencies(string masterProperty, string dependentProperty)
        {
            RegisterDependencies(this, masterProperty, dependentProperty);
        }

        /// <summary>
        /// Mark dependent property to be notified after master property change.
        /// Warning: Not thead safe. You should register all dependencies in the constructor 
        /// before any property change can occur in other thread.
        /// Circular dependencies are not checked.
        /// </summary>
        /// <param name="masterObject">Mater object who contains Master Property</param>
        /// <param name="masterProperty">Master Property or property path. (<example>"Parent.Child.Value"<example>)</param>
        /// <param name="dependentProperty">This property will be notified after Master Property change</param>
        protected void RegisterDependencies(INotifyPropertyChanged masterObject, string masterProperty, string dependentProperty)
        {
            if (masterObject == null)
                throw new ArgumentNullException("masterObject");
            if (masterProperty == null)
                throw new ArgumentNullException("masterProperty");
            if (dependentProperty == null)
                throw new ArgumentNullException("dependentProperty");

            if (masterObject == this && !masterProperty.Contains("."))
                registerDirectDependency(masterProperty, dependentProperty);
            else
                registerChainedDependency(masterObject, masterProperty, dependentProperty);
        }

        /// <summary>
        /// Mark dependent property to be notified after master property change.
        /// Warning: Not thead safe. You should register all dependencies in the constructor 
        /// before any property change can occur in other thread.
        /// Circular dependencies are not checked.
        /// </summary>
        /// <param name="masterProperty">Lambda expression containing Master property path <example>() => Container.Count</example></param>
        /// <param name="dependentProperty">This property will be notified after Master Property change</param>
        protected void RegisterDependencies(Expression<Func<object>> masterProperty, Expression<Func<object>> dependentProperty)
        {
            string dependentPropertyName = GetPropertyName(dependentProperty);

            List<string> propertyChain = new List<string>(1);

            // cyclically parse expression tree to extract sequence of properties.
            // Property chain is reversed - rightmost property first.
            Expression expression = masterProperty.Body;
            while (expression != null)
            {
                if (expression is UnaryExpression && expression.NodeType == ExpressionType.Convert)
                    expression = ((UnaryExpression)expression).Operand as Expression;

                MemberExpression propertyExpression = expression as MemberExpression;
                if (propertyExpression != null && propertyExpression.Member.MemberType == MemberTypes.Property)
                {
                    propertyChain.Add(propertyExpression.Member.Name);
                    expression = propertyExpression.Expression;
                }
                else
                {
                    break;
                }
            }
            // At this moment propertyChain contains chain of properties, expression - expression to get bind object (this in most cases)

            if (propertyChain.Count < 1 || expression == null)
                throw new ArgumentException("Please provide a lambda expression like '() => PropertyName.ChildProperty'");

            // Get master object from master object expression. 
            object masterObject;
            if (expression is ConstantExpression)
                masterObject = ((ConstantExpression)expression).Value;
            else
                masterObject = Expression.Lambda(expression).Compile().DynamicInvoke();

            if (masterObject == null)
                throw new ArgumentException("Cannot register foreign dependency for null objects");

            // If dependency is direct
            if (masterObject == this && propertyChain.Count == 1)
            {
                registerDirectDependency(propertyChain[0], dependentPropertyName);
            }
            else
            {
                IPropertyChangeHandler handler = new PropertyChangeLeafHandler(propertyChain[0], this, dependentPropertyName);
                for (int i = 1; i < propertyChain.Count; i++)
                {
                    handler = new PropertyChangeTraverseHandler(propertyChain[i], handler);
                }
                handler.Bind(masterObject, skipNotify: true);
            }
        }

        protected void RegisterValidator(IValidator validator)
        {
            if (validator == null)
                throw new ArgumentNullException("validator");

            if (validators == null)
                validators = new HashSet<IValidator>();

            validators.Add(validator);
        }

        protected virtual bool Validate()
        {
            if (validators == null)
                return true;

            bool valid = true;
            foreach (var validator in validators)
            {
                if (!validator.IsValid)
                {
                    valid = false;

                    IPropertyValidator propertyValidator = validator as IPropertyValidator;
                    if (propertyValidator != null)
                        raiseErrorsChanged(propertyValidator);
                }
            }
            return valid;
        }

        public static string GetPropertyName(Expression<Func<object>> property)
        {
            // Extracts Property name from property access lambda expression.
            // Object of the property is ignored. No check for null is performed.
            var lambda = property as LambdaExpression;
            if (lambda != null)
            {
                MemberExpression memberExpression;
                if (lambda.Body is UnaryExpression && lambda.Body.NodeType == ExpressionType.Convert)
                    memberExpression = ((UnaryExpression)lambda.Body).Operand as MemberExpression;
                else
                    memberExpression = lambda.Body as MemberExpression;

                if (memberExpression != null)
                {
                    return memberExpression.Member.Name;
                }
            }
            throw new ArgumentException("Please provide a lambda expression like '() => PropertyName'");
        }

        public static string GetPropertyPath(Expression<Func<object>> property)
        {
            StringBuilder sb = new StringBuilder();

            // cyclically parse expression tree to extract sequence of properties.
            // Property chain is reversed - rightmost property first.
            Expression expression = property.Body;
            bool isTargetProperty = true;
            while (expression != null)
            {
                if (expression is UnaryExpression && expression.NodeType == ExpressionType.Convert)
                    expression = ((UnaryExpression)expression).Operand as Expression;

                MemberExpression propertyExpression = expression as MemberExpression;
                if (propertyExpression != null && propertyExpression.Member.MemberType == MemberTypes.Property)
                {
                    if (isTargetProperty == false)
                        sb.Insert(0, ".");

                    sb.Insert(0, propertyExpression.Member.Name);

                    expression = propertyExpression.Expression;
                    isTargetProperty = false;
                }
                else
                {
                    break;
                }
            }
            // At this moment propertyChain contains chain of properties, expression - expression to get bind object (this in most cases)
            string path = sb.ToString();

            if (string.IsNullOrEmpty(path) || expression == null)
                throw new ArgumentException("Please provide a lambda expression like '() => PropertyName.ChildProperty'");

            return path;
        }

        private void registerDirectDependency(string masterProperty, string dependentProperty)
        {

            if (dependencies == null)
                dependencies = new Dictionary<string, List<string>>();

            List<string> dependentProperties;
            if (!dependencies.TryGetValue(masterProperty, out dependentProperties))
            {
                dependentProperties = new List<string>();
                dependencies.Add(masterProperty, dependentProperties);
            }

            dependentProperties.Add(dependentProperty);
        }

        private void registerChainedDependency(INotifyPropertyChanged masterObject, string masterProperty, string dependentProperty)
        {
            // Chained dependency is chain of dependency handlers. 
            // For example we have dependent property 
            // bool Dep {get {return !obj.Property.Value; } }
            // Then new chanined dependency is registered: (obj, "Property.Value", Dep)
            // Two IHandler objects are created: 
            // LeafHandler subscribes to (obj.Property) Value change event and notifies "Dep" property on change
            // TraverseHandler subscribes on obj Property change event and rebind LeafHandler 
            // to new object after change.
            // All properties in the chain must be public. 

            Stack<string> propertyChain = new Stack<string>(masterProperty.Split('.'));

            IPropertyChangeHandler handler = new PropertyChangeLeafHandler(propertyChain.Pop(), this, dependentProperty);
            while (propertyChain.Count > 0)
            {
                handler = new PropertyChangeTraverseHandler(propertyChain.Pop(), handler);
            }
            handler.Bind(masterObject, skipNotify: true);
        }

        #region Dynamic Object

#if DEBUG

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            bool tryGetMemberResult = base.TryGetMember(binder, out result);

            if (tryGetMemberResult == false)
                throw new Exception(string.Format("Unable to get value of property \"{0}\" from \"{1}\".", binder.Name, GetType().Name));

            return tryGetMemberResult;
        }

#endif

        #endregion

        /// <summary>
        /// PropertyChangeHandler object handles PropertyChanged event of bind object
        /// </summary>
        private interface IPropertyChangeHandler
        {
            void Bind(object newMasterObject, bool skipNotify);
        }

        /// <summary>
        /// Handles PropertyChanged event of bind object by calling OnProperyChanged method of dependentObject.
        /// </summary>
        private class PropertyChangeLeafHandler : IPropertyChangeHandler
        {
            object masterObject;
            readonly string masterProperty;
            readonly NotifyPropertyChangedBase dependentObject;
            readonly string dependentProperty;

            public PropertyChangeLeafHandler(string masterProperty, NotifyPropertyChangedBase dependentObject, string dependentProperty)
            {
                this.masterProperty = masterProperty;
                this.dependentObject = dependentObject;
                this.dependentProperty = dependentProperty;
            }

            public void Bind(object newMasterObject, bool skipNotify)
            {
                if (masterObject is INotifyPropertyChanged)
                    ((INotifyPropertyChanged)masterObject).PropertyChanged -= masterObject_PropertyChanged;
                this.masterObject = newMasterObject;
                if (masterObject is INotifyPropertyChanged)
                    ((INotifyPropertyChanged)masterObject).PropertyChanged += masterObject_PropertyChanged;
                if (!skipNotify)
                    dependentObject.OnPropertyChanged(dependentProperty);
            }

            private void masterObject_PropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                if (e.PropertyName == masterProperty)
                    dependentObject.OnPropertyChanged(dependentProperty);
            }
        }

        /// <summary>
        /// Handles PropertyChanged event of bind object rebinding chained handler to the new object, 
        /// get with new masterProperty value
        /// </summary>
        private class PropertyChangeTraverseHandler : IPropertyChangeHandler
        {
            object masterObject;
            readonly string masterProperty;
            readonly IPropertyChangeHandler handler;

            public PropertyChangeTraverseHandler(string masterProperty, IPropertyChangeHandler handler)
            {
                this.masterProperty = masterProperty;
                this.handler = handler;
            }

            public void Bind(object newMasterObject, bool skipNotify)
            {
                if (masterObject is INotifyPropertyChanged)
                    ((INotifyPropertyChanged)masterObject).PropertyChanged -= masterObject_PropertyChanged;
                this.masterObject = newMasterObject;
                if (masterObject is INotifyPropertyChanged)
                    ((INotifyPropertyChanged)masterObject).PropertyChanged += masterObject_PropertyChanged;
                rebindHandler(skipNotify);
            }

            private void masterObject_PropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                if (e.PropertyName == masterProperty)
                {
                    rebindHandler(skipNotify: false);
                }
            }

            private void rebindHandler(bool skipNotify)
            {
                object newTarget = null;
                if (masterObject != null)
                {
                    var property = masterObject.GetType().GetProperty(masterProperty, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
                    if (property != null)
                        newTarget = property.GetValue(masterObject, null);
                }
                handler.Bind(newTarget, skipNotify);
            }
        }

        #region INotifyDataErrorInfo

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
                return null;

            List<IPropertyValidator> propertyValidators = validators.OfType<IPropertyValidator>().Where(v => v.PropertyName == propertyName).ToList();
            if (propertyValidators.Count > 0)
                return propertyValidators.Select(v => v.ShortMessage).ToList();

            return null;
        }

        public bool HasErrors
        {
            get
            {
                if (validators == null)
                    return false;

                return validators.Any(v => !v.IsValid);
            }
        }

        public string GetFirstError()
        {
            IValidator validator = validators.FirstOrDefault(v => !v.IsValid);
            if (validator != null)
                return string.IsNullOrEmpty(validator.Message) ? validator.ShortMessage : validator.Message;

            return null;
        }

        private void raiseErrorsChanged(IPropertyValidator validator)
        {
            var handler = ErrorsChanged;
            if (handler != null)
                handler.Invoke(this, new DataErrorsChangedEventArgs(validator.PropertyName));

        }

        #endregion
    }
}
